﻿using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UndoRedo.UnitTests
{
    [TestClass]
    public class CircularUndoRedoStackTests
    {
        private const int DEFAULT_STACK_SIZE = 10;

        [TestMethod]
        public void VerifyCanAddMoreThanStackSizeChanges()
        {
            var stack = this.CreateFullStack(DEFAULT_STACK_SIZE);
            stack.PushNewChange(0);
        }

        [TestMethod]
        public void VerifyUndoCountCorrectForFullStack()
        {
            var stack = this.CreateFullStack(DEFAULT_STACK_SIZE);
            Assert.AreEqual(stack.MaxSize, stack.UndoableChangesCount);
        }

        [TestMethod]
        public void VerifyCanUndoAllChangesForFullStack()
        {
            var stack = this.CreateFullStack(DEFAULT_STACK_SIZE);
            this.UndoN(stack, stack.MaxSize);
        }

        [TestMethod]
        [ExpectedException(typeof(InvalidOperationException))]
        public void VerifyCanNotUndoMoreChangesThanAreAvailable()
        {
            var stack = this.CreateFullStack(DEFAULT_STACK_SIZE);
            this.UndoN(stack, stack.MaxSize + 1);
        }

        [TestMethod]
        [ExpectedException(typeof(InvalidOperationException))]
        public void VerifyCanNotRedoMoreChangesThanAreAvailable()
        {
            var stack = this.CreateFullStack(DEFAULT_STACK_SIZE);
            stack.Redo();
        }

        [TestMethod]
        public void VerifyUndoCountDecrementAfterUndoCalled()
        {
            var stack = this.CreateFullStack(DEFAULT_STACK_SIZE);

            int startingUndoCount = stack.UndoableChangesCount;
            stack.Undo();
            int endingUndoCount = stack.UndoableChangesCount;

            Assert.AreEqual(startingUndoCount - 1, endingUndoCount);
        }

        [TestMethod]
        public void VerifyRedoCountDecrementAfterRedoCalled()
        {
            var stack = this.CreateFullStack(DEFAULT_STACK_SIZE, 1);

            int startingRedoCount = stack.RedoableChangesCount;
            stack.Redo();
            int endingRedoCount = stack.RedoableChangesCount;

            Assert.AreEqual(startingRedoCount - 1, endingRedoCount);
        }

        [TestMethod]
        public void VerifyOnlyLastMaxSizeChangesKept()
        {
            var stack = this.CreateFullStack(DEFAULT_STACK_SIZE + 1);

            int numExtraChanges = stack.MaxSize / 2;
            this.PushNChanges(stack, numExtraChanges, stack.MaxSize);

            int[] values = this.UndoN(stack, stack.MaxSize);

            int totalChanges = stack.MaxSize + numExtraChanges;
            CollectionAssert.AreEqual(Enumerable.Range(totalChanges - stack.MaxSize, stack.MaxSize).ToArray(), values);

            InvalidOperationException expectedException = null;
            try { stack.Undo(); }
            catch (InvalidOperationException ex)
            { expectedException = ex; }

            Assert.AreNotEqual(null, expectedException);
        }

        [TestMethod]
        public void VerifyNoRedosAfterMultipleRedosAndOneNewChange()
        {
            var stack = this.CreateFullStack(2 * DEFAULT_STACK_SIZE);
            this.UndoN(stack, stack.MaxSize / 2);

            stack.PushNewChange(1);
            Assert.AreEqual(0, stack.RedoableChangesCount);
        }

        [TestMethod]
        public void VerifyUndoUndoesNewChangeAfterMultipleUndosFollowedByANewChange()
        {
            var stack = this.CreateFullStack(2 * DEFAULT_STACK_SIZE);
            this.UndoN(stack, stack.MaxSize / 2);

            int newChange = -1;
            stack.PushNewChange(newChange);
            int value = stack.Undo();

            Assert.AreEqual(newChange, value);
        }

        private int[] UndoN(CircularUndoRedoStack<int> stack, int numToUndo)
        {
            int[] values = new int[numToUndo];
            for (int i = 0; i < numToUndo; i++)
                values[numToUndo - i - 1] = stack.Undo();

            return values;
        }

        private CircularUndoRedoStack<int> CreateFullStack(int stackSize, int initialUndos = 0)
        {
            CircularUndoRedoStack<int> stack = new CircularUndoRedoStack<int>(stackSize);
            this.PushNChanges(stack, stack.MaxSize);

            this.UndoN(stack, initialUndos);

            return stack;
        }

        private void PushNChanges(CircularUndoRedoStack<int> stack, int numChanges, int startValue = 0)
        {
            for (int i = startValue; i < startValue + numChanges; i++)
                stack.PushNewChange(i);
        }
    }
}
